/*************************************************************************/
/*  sproto_bind.cpp                                                         */
/*************************************************************************/
/*                       This file is part of:                           */
/*                           GODOT ENGINE                                */
/*                      https://godotengine.org                          */
/*************************************************************************/
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "sproto_bind.h"

#include "core/error_macros.h"
#include "core/math/math_funcs.h"
#include "core/os/copymem.h"
#include "core/os/os.h"

extern "C" {
#include <sproto.h>
};

#define ENCODE_DEEPLEVEL 64
#define ENCODE_MAXSIZE 0x1000000
#define ENCODE_BUFFERSIZE 2050

Sproto::Sproto() {
	m_sproto = NULL;
	m_packBuffer.resize(ENCODE_BUFFERSIZE);
	m_unpackBuffer.resize(ENCODE_BUFFERSIZE);
	m_encodeBuffer.resize(ENCODE_BUFFERSIZE);
	m_decodeBuffer.resize(ENCODE_BUFFERSIZE);
}

Sproto::~Sproto() {
	if (m_sproto != NULL) {
		sproto_release(m_sproto);
		m_sproto = NULL;
	}
}

void Sproto::clearcache() {
	m_requests.clear();
	m_dispatch_requests.clear();
}

bool Sproto::newsproto(const PoolByteArray &buffer) {
	PoolByteArray::Read r = buffer.read();
	const uint8_t *ptr = r.ptr();
	int sz = buffer.size();
	struct sproto *sp = sproto_create(buffer.read().ptr(), buffer.size());
	if (sp == NULL) {
		ERR_PRINT("create sproto error");
		return false;
	}
	m_sproto = sp;
	return true;
}

bool Sproto::deletesproto() {
	if (m_sproto == NULL) {
		ERR_PRINT("sproto not create");
		return false;
	}
	sproto_release(m_sproto);
	m_sproto = NULL;
	return true;
}

void Sproto::dumpsproto() {
	if (m_sproto == NULL) {
		ERR_PRINT("sproto is NULL");
		return;
	}
	sproto_dump(m_sproto);
}

struct sproto_type *Sproto::get_sproto_type(const String &name) {
	if (m_sproto == NULL) {
		ERR_PRINT("sproto is NULL");
		return NULL;
	}
	return sproto_type(m_sproto, name.utf8().get_data());
	;
}

struct encode_ud {
	struct sproto_type *st;
	Variant tbl;
	const char *array_tag;
	int array_index;
	int deep;
};

static int encode_callback(const struct sproto_arg *args) {
	struct encode_ud *self = (struct encode_ud *)args->ud;
	Variant tbl = self->tbl;
	if (self->deep >= ENCODE_DEEPLEVEL) {
		ERR_PRINT("The table is too deep");
		return SPROTO_CB_ERROR;
	}

	Variant target;
	Variant &tagValue = tbl.operator Dictionary()[args->tagname];
	if (args->index > 0) {
		if (args->tagname != self->array_tag) {
			self->array_tag = args->tagname;
			Variant::Type tagType = tagValue.get_type();
			if (tagType != Variant::ARRAY && tagType != Variant::DICTIONARY) {
				self->array_index = 0;
				return SPROTO_CB_NIL;
			}
			if ((tagType == Variant::ARRAY && tagValue.operator Array().size() == 0) || (tagType == Variant::DICTIONARY && tagValue.operator Dictionary().size() == 0)) {
				self->array_index = 0;
				return SPROTO_CB_NOARRAY;
			}
			if (tagType == Variant::DICTIONARY) {
				tbl.operator Dictionary()[args->tagname] = tagValue.operator Dictionary().values();
			}
			target = tagValue.operator Array()[args->index - 1];
			if (target.get_type() == Variant::NIL) {
				return SPROTO_CB_NIL;
			}
		} else {
			if (tagValue.operator Array().size() < args->index) {
				return SPROTO_CB_NIL;
			} else {
				target = tagValue.operator Array()[args->index - 1];
			}
		}
	} else {
		target = tagValue;
	}

	if (target.get_type() == Variant::NIL) {
		return SPROTO_CB_NIL;
	}

	switch (args->type) {
		case SPROTO_TINTEGER: {
			if (target.get_type() != Variant::INT && target.get_type() != Variant::REAL) {
				ERR_PRINT(String(args->tagname) + "(" + String::num(args->tagid) + ")should be a int/real.");
				return SPROTO_CB_ERROR;
			}
			int64_t v;
			if (args->extra) {
				if (target.get_type() == Variant::INT) {
					v = (int64_t)(target.operator signed int() * args->extra);
				} else {
					v = (int64_t)(Math::round(target.operator double() * args->extra));
				}
			} else {
				if (target.get_type() == Variant::INT) {
					v = (int64_t)(target.operator signed int());
				} else {
					v = (int64_t)(Math::round(target.operator double()));
				}
			}
			int64_t vh = v >> 31;
			if (vh == 0 || vh == -1) {
				*(uint32_t *)args->value = (uint32_t)v;
				return 4;
			} else {
				*(uint64_t *)args->value = (uint64_t)v;
				return 8;
			}
		}
		case SPROTO_TBOOLEAN: {
			if (target.get_type() != Variant::BOOL) {
				ERR_PRINT(String(args->tagname) + "(" + String::num(args->tagid) + ")should be a Bool.");
				return SPROTO_CB_ERROR;
			}
			*(int *)args->value = target.operator bool();
			return 4;
		}
		case SPROTO_TSTRING: {
			if (target.get_type() != Variant::STRING && target.get_type() != Variant::POOL_BYTE_ARRAY) {
				ERR_PRINT(String(args->tagname) + "(" + String::num(args->tagid) + ")should be a String/PoolByteArray.");
				return SPROTO_CB_ERROR;
			}
			int sz = 0;
			if (args->extra != 0) {
				sz = target.operator PoolByteArray().size();
				if (sz > args->length)
					return SPROTO_CB_ERROR;
				memcpy(args->value, target.operator PoolByteArray().read().ptr(), sz);
			} else {
				sz = target.operator String().utf8().length();
				if (sz > args->length)
					return SPROTO_CB_ERROR;
				memcpy(args->value, target.operator String().utf8().get_data(), sz);
			}

			return sz;
		}
		case SPROTO_TSTRUCT: {
			struct encode_ud sub;
			memset(&sub, 0x00, sizeof(sub));
			sub.st = args->subtype;
			sub.deep = self->deep + 1;
			sub.tbl = target;
			int r = sproto_encode(args->subtype, args->value, args->length, encode_callback, &sub);
			if (r < 0) {
				return SPROTO_CB_ERROR;
			}
			return r;
		}
		default:
			ERR_PRINT("Invalid field type " + String::num(args->type));
			break;
	}

	return SPROTO_CB_ERROR;
}

PoolByteArray Sproto::encode(const String &name, const Dictionary &data) {
	PoolByteArray result;

	struct sproto_type *st = get_sproto_type(name);
	if (st == NULL) {
		ERR_PRINT("this sproto type is NULL");
		return result;
	}

	struct encode_ud self;
	memset(&self, 0x00, sizeof(struct encode_ud));
	self.tbl = Variant(data);

	for (;;) {
		int r = 0;
		self.array_tag = NULL;
		self.array_index = 0;
		self.deep = 0;

		{
			PoolByteArray::Write w = m_encodeBuffer.write();
			r = sproto_encode(st, w.ptr(), m_encodeBuffer.size(), encode_callback, &self);
		}

		if (r < 0) {
			m_encodeBuffer.resize(m_encodeBuffer.size() * 2);
		} else {
			result = m_encodeBuffer;
			result.resize(r);
			return result;
		}
	}

	return result;
}

struct decode_ud {
	Variant result;
	const char *array_tag;
	int deep;
	int array_index;
	int result_index;
	int mainindex_tag;
	int key_index;
};

static int decode_callback(const struct sproto_arg *args) {
	struct decode_ud *self = (struct decode_ud *)args->ud;
	Variant &result = self->result;
	Variant value;
	if (self->deep >= ENCODE_DEEPLEVEL) {
		ERR_PRINT("The table is too deep");
		return SPROTO_CB_ERROR;
	}
	if (args->index != 0) {
		if (args->tagname != self->array_tag) {
			self->array_tag = args->tagname;
			if (args->mainindex >= 0) {
				result.operator Dictionary()[args->tagname] = Dictionary();
			} else {
				result.operator Dictionary()[args->tagname] = Array();
			}
			if (args->index < 0) {
				return 0;
			}
		}
	}

	switch (args->type) {
		case SPROTO_TINTEGER: {
			if (args->extra != 0) {
				int64_t v = *(int64_t *)args->value;
				value = (double)v / (double)args->extra;
			} else {
				int64_t v = *(int64_t *)args->value;
				value = v;
			}
			break;
		}
		case SPROTO_TBOOLEAN: {
			value = (bool)(*(uint64_t *)args->value);
			break;
		}
		case SPROTO_TSTRING: {
			if (args->extra != 0) {
				PoolByteArray strBuf;
				strBuf.resize(args->length);
				PoolByteArray::Write w = strBuf.write();
				memcpy(w.ptr(), args->value, args->length);
				value = strBuf;
			} else {
				char *tmp = (char *)malloc(args->length + 1);
				memset(tmp, 0x00, args->length + 1);
				memcpy(tmp, args->value, args->length);
				String str(tmp);
				int len = str.length();
				value = str;
				free(tmp);
				tmp = NULL;
			}
			break;
		}
		case SPROTO_TSTRUCT: {
			struct decode_ud sub;
			int r;
			sub.deep = self->deep + 1;
			sub.array_index = 0;
			sub.array_tag = NULL;
			sub.result = Dictionary();
			if (args->mainindex >= 0) {
				sub.mainindex_tag = args->mainindex;
				r = sproto_decode(args->subtype, args->value, args->length, decode_callback, &sub);
				if (r < 0 || r != args->length) {
					return r;
				}
				value = sub.result;
			} else {
				sub.mainindex_tag = -1;
				sub.key_index = 0;
				r = sproto_decode(args->subtype, args->value, args->length, decode_callback, &sub);
				if (r < 0) {
					return SPROTO_CB_ERROR;
				}
				if (r != args->length) {
					return r;
				}
				value = sub.result;
			}
			break;
		}
		default: {
			ERR_PRINT(args->tagname + String(": invalid type"));
			break;
		}
	}
	if (args->index > 0) {
		if (args->mainindex >= 0) {
			String _mainindex = value.operator Dictionary()["_mainindex"];
			value.operator Dictionary().erase("_mainindex");
			result.operator Dictionary()[args->tagname].operator Dictionary()[_mainindex] = value;
		} else {
			result.operator Dictionary()[args->tagname].operator Array().push_back(value);
		}
	} else {
		if (self->mainindex_tag >= 0 && self->mainindex_tag == args->tagid) {
			result.operator Dictionary()["_mainindex"] = value;
		}
		result.operator Dictionary()[args->tagname] = value;
	}

	return 0;
}

Dictionary Sproto::decode(const String &name, const PoolByteArray &buffer) {
	Dictionary result;
	result["size"] = -1;

	struct sproto_type *st = get_sproto_type(name);
	if (st == NULL) {
		ERR_PRINT(String("sproto type[") + name + String("] is NULL"));
		return result;
	}

	struct decode_ud self;
	self.result = Dictionary();
	self.deep = 0;
	self.array_index = 0;
	self.array_tag = NULL;
	self.mainindex_tag = -1;
	self.key_index = 0;
	PoolByteArray::Read r = buffer.read();
	int ret = sproto_decode(st, r.ptr(), buffer.size(), decode_callback, &self);
	if (ret < 0) {
		ERR_PRINT(String("decode error ret=") + String::num(ret));
		return result;
	}
	result["size"] = ret;
	result["data"] = self.result;

	return result;
}

static void expand_buffer(PoolByteArray &buffer, int nsz) {
	PoolByteArray::Read r = buffer.read();
	if (nsz > ENCODE_MAXSIZE) {
		ERR_PRINT("expand_buffer err, object is too large, max size limit " + String::num(ENCODE_MAXSIZE));
		return;
	}
	buffer.resize(nsz);
}

PoolByteArray Sproto::pack(const PoolByteArray &buffer) {
	PoolByteArray result;
	int bytes;
	int sz = buffer.size();
	int maxsz = (sz + 2047) / 2048 * 2 + sz;
	if (m_packBuffer.size() < maxsz) {
		expand_buffer(m_packBuffer, maxsz);
	}
	{
		PoolByteArray::Write w = m_packBuffer.write();
		bytes = sproto_pack(buffer.read().ptr(), sz, w.ptr(), maxsz);
	}
	if (bytes > maxsz) {
		ERR_PRINT("pack err, return size = " + String::num(bytes) + ", max size limit " + String::num(ENCODE_MAXSIZE));
		return result;
	}

	result.resize(bytes);
	PoolByteArray::Write w = result.write();
	memcpy(w.ptr(), m_packBuffer.read().ptr(), bytes);

	return result;
}

PoolByteArray Sproto::unpack(const PoolByteArray &buffer) {
	PoolByteArray result;
	int ret = 0;
	int sz = buffer.size();
	int osz = m_unpackBuffer.size();
	{
		PoolByteArray::Read r = buffer.read();
		PoolByteArray::Write w = m_unpackBuffer.write();
		ret = sproto_unpack(r.ptr(), sz, w.ptr(), osz);
	}
	if (ret < 0) {
		ERR_PRINT("Invalid unpack stream ret=" + String::num(ret));
		return result;
	}
	if (ret > osz) {
		expand_buffer(m_unpackBuffer, ret);
		PoolByteArray::Read r = buffer.read();
		PoolByteArray::Write w = m_unpackBuffer.write();
		ret = sproto_unpack(r.ptr(), sz, w.ptr(), ret);
	}
	if (ret < 0) {
		ERR_PRINT("Invalid unpack stream ret=" + String::num(ret));
		return result;
	}
	result.resize(ret);
	memcpy(result.write().ptr(), m_unpackBuffer.read().ptr(), ret);
	return result;
}

Dictionary Sproto::protocol(const Variant &name) {
	Dictionary result;
	int tag;
	const char *pname;
	struct sproto_type *request;
	struct sproto_type *response;
	if (m_sproto == NULL) {
		return result;
	}
	if (name.get_type() == Variant::INT) {
		tag = name.operator signed int();
		pname = sproto_protoname(m_sproto, tag);
		if (pname == NULL) {
			return result;
		}
	} else if (name.get_type() == Variant::STRING) {
		pname = name.operator String().utf8().get_data();
		if (pname == NULL) {
			return result;
		}
		tag = sproto_prototag(m_sproto, pname);
		if (tag < 0) {
			return result;
		}
	} else {
		return result;
	}
	result["pname"] = String(pname);
	result["tag"] = tag;
	//result["request"] = String(pname) + ".request";
	//result["response"] = String(pname) + ".response";

	//if the request is a sproto_type
	request = sproto_protoquery(m_sproto, tag, SPROTO_REQUEST);
	if (request != NULL) {
		result["request"] = String(sproto_name(request));
	}

	//if the response is a sproto_type
	response = sproto_protoquery(m_sproto, tag, SPROTO_RESPONSE);
	if (response != NULL) {
		result["response"] = String(sproto_name(response));
	}

	return result;
}

bool Sproto::host(const String &name) {
	struct sproto_type *st = get_sproto_type(name);
	if (st == NULL) {
		ERR_PRINT(String("sproto type[") + name + String("] is NULL"));
		return false;
	}
	m_hostName = name;
	return true;
}

Dictionary Sproto::dispatch(const PoolByteArray &buffer) {
	Dictionary result = Dictionary();
	result["type"] = NONE;
	struct sproto_type *st = get_sproto_type(m_hostName);
	if (st == NULL) {
		ERR_PRINT(String("sproto type[") + m_hostName + String("] is NULL"));
		return result;
	}
	if (buffer.size() == 0) {
		ERR_PRINT("Passed buffer is NULL");
		return result;
	}
	PoolByteArray bytes = unpack(buffer);
	if (bytes.size() == 0) {
		ERR_PRINT("Buffer unpack failed");
		return result;
	}
	
	Dictionary headerRet = decode(m_hostName, bytes);
	if (!headerRet.has("size") || headerRet["size"].operator int() == -1) {
		ERR_PRINT("Buffer decode header failed");
		return result;
	}
	m_header.clear();
	m_header = headerRet["data"].operator Dictionary();
	if (m_header.empty()) {
		ERR_PRINT("header is Empty");
		return result;
	}
	PoolByteArray remainBytes;
	int headerSize = headerRet["size"].operator int();
	if (headerSize < bytes.size()) {
		remainBytes = bytes.subarray(headerSize,  -1);
	}
	
	if (m_header.has("session") && m_header.has("type")) {
		Dictionary dataRet;
		Dictionary proto = protocol(m_header["type"]);
		if (proto.has("request") && remainBytes.size() > 0) {
			dataRet = decode(proto["request"], remainBytes);
		}
		result["type"] = REQUEST;
		result["protocol"] = proto["pname"];
		result["tag"] = proto["tag"];
		result["session"] = m_header["session"];
		if (dataRet.has("data")) {
			result["data"] = dataRet["data"];
		}
		if (m_header.has("ud")) {
			result["ud"] = m_header["ud"];
		}
		m_dispatch_requests[m_header["session"]] = proto;
		return result;
	} else if (m_header.has("session") && !m_header.has("type")) {
		if (!m_requests.has(m_header["session"])) {
			return result;
		}
		Dictionary dataRet;
		Dictionary proto = m_requests[m_header["session"]];
		if (proto.has("response") && remainBytes.size() > 0) {
			dataRet = decode(proto["response"], remainBytes);
		}
		result["type"] = RESPONSE;
		result["protocol"] = proto["pname"];
		result["tag"] = proto["tag"];
		result["session"] = m_header["session"];
		if (dataRet.has("data")) {
			result["data"] = dataRet["data"];
		}
		if (m_header.has("ud")) {
			result["ud"] = m_header["ud"];
		}
		m_requests.erase(m_header["session"]);
		return result;
	} else if (!m_header.has("session") && m_header.has("type")) {
		Dictionary dataRet;
		Dictionary proto = protocol(m_header["type"]);
		if (proto.has("response") && remainBytes.size() > 0) {
			dataRet = decode(proto["response"], remainBytes);
		}
		result["type"] = RESPONSE;
		result["protocol"] = proto["pname"];
		result["tag"] = proto["tag"];
		if (dataRet.has("data")) {
			result["data"] = dataRet["data"];
		}
		if (m_header.has("ud")) {
			result["ud"] = m_header["ud"];
		}
		return result;
	}
	return result;
}

PoolByteArray Sproto::request(const String &name, const Variant &data, const Variant &ud) {
	m_session++;
	if (m_session >= 2147483647) {
		m_session = 1;
	}
	m_header.clear();
	Dictionary proto = protocol(name);
	m_header["type"] = proto["tag"];
	m_header["session"] = m_session;
	if (ud.get_type() != Variant::NIL) {
		m_header["ud"] = ud;
	}
	PoolByteArray bytes = encode(m_hostName, m_header);
	if (proto.has("request") && data.get_type() != Variant::NIL) {
		PoolByteArray dataBytes = encode(proto["request"], data);
		bytes.append_array(dataBytes);
	}
	m_requests[m_session] = proto;
	return pack(bytes);
}

PoolByteArray Sproto::response(const String &name, const Variant &data, const Variant &ud, int session) {
	m_header.clear();
	Dictionary proto;
	if (session > 0 && m_dispatch_requests.has(session)) {
		proto = m_dispatch_requests[session].operator Dictionary();
		m_header["type"] = proto["tag"];
		m_header["session"] = session;
		m_dispatch_requests.erase(session);
	} else {
		proto = protocol(name);
	}
	if (ud.get_type() != Variant::NIL) {
		m_header["ud"] = ud;
	}
	PoolByteArray bytes = encode(m_hostName, m_header);
	if (proto.has("response") && data.get_type() != Variant::NIL) {
		PoolByteArray dataBytes = encode(proto["response"], data);
		bytes.append_array(dataBytes);
	}
	return pack(bytes);
}

void Sproto::_bind_methods() {
	ClassDB::bind_method(D_METHOD("clearcache"), &Sproto::clearcache);
	ClassDB::bind_method(D_METHOD("newsproto", "bytes"), &Sproto::newsproto);
	ClassDB::bind_method(D_METHOD("deletesproto"), &Sproto::deletesproto);
	ClassDB::bind_method(D_METHOD("dumpsproto"), &Sproto::dumpsproto);
	ClassDB::bind_method(D_METHOD("encode", "type", "param"), &Sproto::encode);
	ClassDB::bind_method(D_METHOD("decode", "type", "bytes"), &Sproto::decode);
	ClassDB::bind_method(D_METHOD("pack", "bytes"), &Sproto::pack);
	ClassDB::bind_method(D_METHOD("unpack", "bytes"), &Sproto::unpack);
	ClassDB::bind_method(D_METHOD("protocol", "nameOrTag"), &Sproto::protocol);
	ClassDB::bind_method(D_METHOD("host", "package"), &Sproto::host, DEFVAL("package"));
	ClassDB::bind_method(D_METHOD("dispatch", "bytes"), &Sproto::dispatch);
	ClassDB::bind_method(D_METHOD("request", "name", "data", "ud"), &Sproto::request, DEFVAL(Variant()), DEFVAL(Variant()));
	ClassDB::bind_method(D_METHOD("response", "name", "data", "ud", "session"), &Sproto::response, DEFVAL(""), DEFVAL(Variant()), DEFVAL(Variant()), DEFVAL(0));

	BIND_ENUM_CONSTANT(NONE);
	BIND_ENUM_CONSTANT(REQUEST);
	BIND_ENUM_CONSTANT(RESPONSE);
}
